"""Block blocking calls being done in asyncio."""

import builtins
from contextlib import suppress
from http.client import HTTPConnection
import importlib
import sys
import threading
import time
from typing import Any

from .helpers.frame import get_current_frame
from .util.loop import protect_loop

_IN_TESTS = "unittest" in sys.modules

ALLOWED_FILE_PREFIXES = ("/proc",)


def _check_import_call_allowed(mapped_args: dict[str, Any]) -> bool:
    # If the module is already imported, we can ignore it.
    return bool((args := mapped_args.get("args")) and args[0] in sys.modules)


def _check_file_allowed(mapped_args: dict[str, Any]) -> bool:
    # If the file is in /proc we can ignore it.
    args = mapped_args["args"]
    path = args[0] if type(args[0]) is str else str(args[0])  # noqa: E721
    return path.startswith(ALLOWED_FILE_PREFIXES)


def _check_sleep_call_allowed(mapped_args: dict[str, Any]) -> bool:
    #
    # Avoid extracting the stack unless we need to since it
    # will have to access the linecache which can do blocking
    # I/O and we are trying to avoid blocking calls.
    #
    # frame[0] is us
    # frame[1] is raise_for_blocking_call
    # frame[2] is protected_loop_func
    # frame[3] is the offender
    with suppress(ValueError):
        return get_current_frame(4).f_code.co_filename.endswith("pydevd.py")
    return False


def enable() -> None:
    """Enable the detection of blocking calls in the event loop."""
    loop_thread_id = threading.get_ident()
    # Prevent urllib3 and requests doing I/O in event loop
    HTTPConnection.putrequest = protect_loop(  # type: ignore[method-assign]
        HTTPConnection.putrequest, loop_thread_id=loop_thread_id
    )

    # Prevent sleeping in event loop. Non-strict since 2022.02
    time.sleep = protect_loop(
        time.sleep,
        strict=False,
        check_allowed=_check_sleep_call_allowed,
        loop_thread_id=loop_thread_id,
    )

    if not _IN_TESTS:
        # Prevent files being opened inside the event loop
        builtins.open = protect_loop(  # type: ignore[assignment]
            builtins.open,
            strict_core=False,
            strict=False,
            check_allowed=_check_file_allowed,
            loop_thread_id=loop_thread_id,
        )
        # unittest uses `importlib.import_module` to do mocking
        # so we cannot protect it if we are running tests
        importlib.import_module = protect_loop(
            importlib.import_module,
            strict_core=False,
            strict=False,
            check_allowed=_check_import_call_allowed,
            loop_thread_id=loop_thread_id,
        )
